// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MetricsTestRunner.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
// uncomment this to run metrics on dektop machines
#define DisableMetricsOnDesktopMachines

namespace Test.Robotics.Metrics
{
    using System;
    using System.Collections.Generic;
    using System.Data.Linq;
    using System.Diagnostics;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization;
    using Microsoft.Robotics.Runtime;
    using Microsoft.Robotics.Tracing;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Test.Robotics.Runtime;

    /// <summary>
    /// Provides methods for running metrics and recording results in a database
    /// </summary>
    public static class MetricsTestRunner
    {
        /// <summary>
        /// Returns the configuration of a given run from the database
        /// </summary>
        /// <param name="configurationName">The name of the configuration to retrieve</param>
        /// <param name="allowRunningFromAnywhere">If true - this method will not throw if ran from a dev box</param>
        /// <returns>The data object representing the row</returns>
        public static Configuration GetConfigurationByName(string configurationName, bool allowRunningFromAnywhere = false)
        {
            Configuration config = GetConfigurationsByName(configurationName, allowRunningFromAnywhere).FirstOrDefault();
            return config;
        }

        /// <summary>
        /// Returns the configuration of a given run from the database
        /// </summary>
        /// <param name="configurationId">The id of the configuration to retrieve</param>
        /// <returns>The data object representing the row</returns>
        public static Configuration GetConfigurationById(int configurationId)
        {
            CheckMachine();

            MetricsDataContext dataContext = new MetricsDataContext(Settings.Default.MetricsConnectionString);
            IEnumerable<Configuration> result = dataContext.Configurations.Where(c => c.Id == configurationId).AsEnumerable();

            if (result.Count() > 1)
            {
                throw new InvalidOperationException("There is more than one configuration with the same ID in the database");
            }

            return result.FirstOrDefault();
        }

        /// <summary>
        /// Returns all the configurations for a given run from the database
        /// </summary>
        /// <param name="configurationName">The name of the configurations to retrieve</param>
        /// <param name="allowRunningFromAnywhere">If true - this method will not throw if ran from a dev box</param>
        /// <returns>The set of data objects representing the row</returns>
        public static IEnumerable<Configuration> GetConfigurationsByName(string configurationName, bool allowRunningFromAnywhere = false)
        {
            if (!allowRunningFromAnywhere)
            {
                CheckMachine();
            }

            MetricsDataContext dataContext = new MetricsDataContext(Settings.Default.MetricsConnectionString);
            IQueryable<Configuration> configs = dataContext.Configurations.Where(c => c.Name == configurationName);

            // since we don't expect that many configurations with the same name (usually there is one per robot used for recording) 
            // run the query now and return an enumerable (instead of returning a deferred query as IQueryable)
            IEnumerable<Configuration> result = configs.AsEnumerable();
            return result;
        }

        /// <summary>
        /// Returns all the configurations for a given run from the database
        /// </summary>
        /// <param name="configurationName">The name of the configurations to retrieve</param>
        /// <param name="robot">The name of the robot</param>
        /// <returns>The set of data objects representing the row</returns>
        public static IEnumerable<Configuration> GetConfigurations(string configurationName, string robot)
        {
            CheckMachine();

            MetricsDataContext dataContext = new MetricsDataContext(Settings.Default.MetricsConnectionString);
            IQueryable<Configuration> configs = dataContext.Configurations.Where(c => c.Name == configurationName && c.Robot == robot);

            // since we don't expect that many configurations with the same name (usually there is one per robot used for recording) 
            // run the query now and return an enumerable (instead of returning a deferred query as IQueryable)
            IEnumerable<Configuration> result = configs.AsEnumerable();
            return result;
        }

        /// <summary>
        /// Runs a manifest and monitors for completion. Persists all reported results to SQL.
        /// </summary>
        /// <param name="manifest">The manifest to run</param>
        /// <param name="configuration">The metrics configuration for the run</param>
        /// <param name="timeout">The timeout for the run</param>
        /// <param name="offline">true if this is an offline run, false for a real-time run</param>
        public static void Run(Manifest manifest, Configuration configuration, TimeSpan timeout, bool offline)
        {
            CheckMachine();

            if (!configuration.IsEnabled)
            {
                Assert.Inconclusive(string.Format("Configuration {0} is disabled and was skipped.", configuration.Id));
            }

            TraceOut.Info(
                TraceContexts.Test,
                "Configuration: {0} {1}, {2} iterations. Recording:{3}, Ground truth:{4}, Configuration:{5}",
                configuration.Id,
                configuration.Name,
                configuration.IterationCount,
                configuration.Recording,
                configuration.GroundTruthSource,
                configuration.ConfigurationData);

            MetricsController resultGenerator;
            MessageCollectorAgent<CompletionMessage> completionAgent;
            AddMetricsResultsCompletionAgent(manifest, out resultGenerator, out completionAgent);

            // add an agent to write to sql
            DelegateAgent<MetricsResultMessage> sqlWriter = new DelegateAgent<MetricsResultMessage>(
                "sqlWriter",
                (IProducer<MetricsResultMessage>)resultGenerator,
                result => WriteToSql(result, configuration));
            manifest.Agents.Add(sqlWriter);
            manifest.ActiveAgents.Add(sqlWriter.Name);

            RunManifest(manifest, timeout, offline, completionAgent);
        }

        /// <summary>
        /// Runs the given manifest until timeout and fails if there is a timeout.
        /// </summary>
        /// <param name="manifest">Manifest to run.</param>
        /// <param name="timeout">Timeout for manifest</param>
        /// <param name="offline">Indicates whether to run the pipeline in offline mode</param>
        /// <param name="completionAgent">The completion agent to wait for metrics run to complete.</param>
        /// <remarks>Will throw exception if showUI is true but there is no agent called "UserInterface" in the manifest</remarks>
        public static void RunManifest(Manifest manifest, TimeSpan timeout, bool offline, MessageCollectorAgent<CompletionMessage> completionAgent)
        {
            // load the manifest and activate the agents
            AgentLocator locator = new AgentLocator(offline);
            locator.LoadFrom(manifest);

            // we can now wait for completion
            bool finished = completionAgent.Wait((int)timeout.TotalMilliseconds);

            // were there any failures?
            IEnumerable<string> failures = from m in completionAgent.Results where m.IsFailure select m.FailureReason;
            locator.Deactivate(); // let any exceptions bubble up 
            if (failures.Count() > 0)
            {
                Assert.Fail("Metrics failure(s): {0}", string.Join("\n", failures));
            }

            if (!finished)
            {
                throw new Exception("Metrics session timed out.");
            }
        }

        /// <summary>
        /// Adds the results generator to the manifest.
        /// </summary>
        /// <param name="manifest">The manifest to add to</param>
        /// <param name="resultGenerator">The metrics result generator which was added</param>
        /// <param name="completionAgent">The completion agent listening to the result generator</param>
        public static void AddMetricsResultsCompletionAgent(Manifest manifest, out MetricsController resultGenerator, out MessageCollectorAgent<CompletionMessage> completionAgent)
        {
            // find the metrics controller
            resultGenerator = (MetricsController)manifest.Agents.FirstOrDefault(agent => agent is MetricsController);
            if (resultGenerator == null)
            {
                throw new Exception("Manifest does not contain a MetricsController agent");
            }

            TraceOut.Info(TraceContexts.Test, "Metrics controller agent: {0}", resultGenerator.Name);

            // add a collector to monitor the pipeline execution
            completionAgent = new MessageCollectorAgent<CompletionMessage>(resultGenerator);
            manifest.Agents.Add(completionAgent);
            manifest.ActiveAgents.Add(completionAgent.Name);
        }

        /// <summary>
        /// Writes a metrics result to SQL
        /// </summary>
        /// <param name="result">The result to persist</param>
        /// <param name="configuration">The configuration of the run that produced the result</param>
        private static void WriteToSql(MetricsResultMessage result, Configuration configuration)
        {
            Result sqlRow = new Result();
            sqlRow.Accuracy = result.Accuracy;
            sqlRow.Precision = result.Precision;
            sqlRow.Weight = result.Weight;
            sqlRow.BuildNumber = Assembly.GetExecutingAssembly().GetName().Version.ToString();
            sqlRow.ConfigurationId = configuration.Id;
            sqlRow.DebugData = result.DebugData;
            sqlRow.Duration = result.Duration;
            sqlRow.Machine = Environment.MachineName;
            sqlRow.Robot = result.Robot;
            sqlRow.Run = result.RunId;
            sqlRow.Time = DateTime.Now;
            sqlRow.Type = result.Type;
            sqlRow.GroundTruthId = result.GroundTruthId;

            MetricsDataContext context = new MetricsDataContext(Settings.Default.MetricsConnectionString);
            context.Results.InsertOnSubmit(sqlRow);
            context.SubmitChanges();
        }

        /// <summary>
        /// Validates that the test can run on this machine 
        /// </summary>
        private static void CheckMachine()
        {
            if (!(Environment.MachineName.StartsWith("b115", StringComparison.InvariantCultureIgnoreCase) || 
                Environment.MachineName.StartsWith("lbr", StringComparison.InvariantCultureIgnoreCase) ||
                Environment.MachineName.StartsWith("cnc-", StringComparison.InvariantCultureIgnoreCase)))
            {
                Assert.Inconclusive("This test case is enabled only on lab machines. Comment out this Assert if you really want to run metrics locally.");
            }
        }
    }
}
